/*
 * BTServerNetworkManager.java
 *    Author: nfk
 *      Date: Sep 19, 2008
 *      
 *   Classes:
 *      BTServerNetworkManager
 */

package at.momberban.game.me;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;


/**
 * A Bluetooth network manager server.
 * @author nfk
 */
public final class BTServerNetworkManager implements ServerNetworkManager, Runnable {

    static {
        try {
            LocalDevice.getLocalDevice().setDiscoverable(DiscoveryAgent.GIAC);
            supportsBluetooth = true;
        } catch (final BluetoothStateException ex) {
            supportsBluetooth = false;
        }

        if (supportsBluetooth()) {
            nm = new BTServerNetworkManager();
        }
    }
    private static boolean                supportsBluetooth;
    private static BTServerNetworkManager nm;
    public static final String            uuid      = "a00914d6469143dcb1fb85fd4a994594";

    private static final String           listenURL = "btspp://localhost:" + uuid + ";name=MomberBanServer";
    private boolean                       isListening;
    private StreamConnectionNotifier      listener;

    private EventGenerator                eventListener;
    private Vector                        connections;
    private Vector                        outputStreams;

    private Sender                        sender;


    /**
     * Returns the network manager instance.
     * @return the network manager instance.
     */
    public static BTServerNetworkManager getManager() {
        return nm;
    }


    /**
     * @see at.momberban.game.me.ServerNetworkManager#isListening()
     */
    public boolean isListening() {
        return this.isListening;
    }


    /**
     * @see at.momberban.game.me.ServerNetworkManager#listen()
     */
    public void listen() throws IOException {
        if (isListening()) {
            throw new IllegalStateException("Already listening.");
        }

        this.listener = (StreamConnectionNotifier) Connector.open(listenURL);
        new Thread(this).start();
    }


    /**
     * @see at.momberban.game.me.NetworkManager#disconnect()
     */
    public void disconnect() {
        OutputStream os = null;
        for (int i = 0; i < this.outputStreams.size(); i++) {
            os = (OutputStream) this.outputStreams.elementAt(i);

            try {
                os.close();
            } catch (final IOException ex) {
                // Ignore
            }

            this.outputStreams.removeElement(os);
        }

        StreamConnection sc = null;
        for (int i = 0; i < this.connections.size(); i++) {
            sc = (StreamConnection) this.connections.elementAt(i);

            try {
                sc.close();
            } catch (final IOException ex) {
                // Ignore
            }

            this.connections.removeElement(sc);
        }

        stop();
    }


    /**
     * @see at.momberban.game.me.NetworkManager#receive()
     */
    public String receive() throws IOException {
        throw new UnsupportedOperationException();
    }


    /**
     * @see at.momberban.game.me.NetworkManager#send(java.lang.String)
     */
    public void send(final String s) throws IOException {
        if (!isListening() || this.sender == null) {
            throw new IllegalStateException("Not listening || no sender active.");
        }

        Logger.log("try to send: " + s);
        this.sender.setSendData(s);
    }


    /** Forbid construction. */
    private BTServerNetworkManager() {
        this.connections = new Vector();
        this.outputStreams = new Vector();
    }


    /**
     * @see java.lang.Runnable#run()
     */
    public void run() {
        this.isListening = true;
        StreamConnection connection = null;

        this.sender = new Sender();
        this.sender.start();

        while (this.isListening) {
            try {
                connection = (StreamConnection) this.listener.acceptAndOpen();
                this.connections.addElement(connection);
                this.outputStreams.addElement(connection.openOutputStream());
                new Receiver(connection).start();

            } catch (final IOException ex) {
                break;
            }
        }

        stop();
    }


    /**
     * @see at.momberban.game.me.ServerNetworkManager#stop()
     */
    public void stop() {
        if (isListening()) {
            this.isListening = false;

            try {
                this.listener.close();
            } catch (final IOException ex) {
                // Ignore
            }
        }
    }


    /**
     * @see at.momberban.game.me.NetworkManager#setEventListener(at.momberban.game.me.EventGenerator)
     */
    public void setEventListener(final EventGenerator listener) {
        this.eventListener = listener;
    }

    /**
     * A simple class to for threaded receival.
     * @author nfk
     */
    private final class Receiver extends Thread {

        private final InputStream is;


        /**
         * Constructs a new instance of Receiver.
         * @param sc the stream connection to handle
         * @throws NullPointerException if sc is null
         * @throws IllegalStateException if no event listener is set
         */
        public Receiver(final StreamConnection sc) {
            if (sc == null) {
                throw new NullPointerException("sc must not be null.");
            }

            if (eventListener == null) {
                throw new IllegalStateException("No event listener.");
            }

            try {
                this.is = sc.openInputStream();
            } catch (final IOException ex) {
                throw new IllegalStateException("Opening input stream failed.");
            }
        }


        /**
         * @see java.lang.Thread#run()
         */
        public void run() {
            String s = null;

            while (isListening) {
                try {
                    s = StringUtilities.readLine(is);
                    eventListener.write(s);
                    Logger.log("Server: Received " + s + "!");
                } catch (final IOException ex) {
                    break;
                }
            }

            try {
                is.close();
            } catch (IOException ex) {
                // Ignore
            }
        }

    }

    /**
     * A thread class to broadcast strings to a handful of output streams.
     * @author nfk
     */
    private final class Sender extends Thread {

        private Vector queue = new Vector();
        private int    start = 0;


        /**
         * @see java.lang.Thread#run()
         */
        public void run() {
            String[] queueClone;
            OutputStream[] streamsClone;

            while (isListening) {
                synchronized (this) {
                    while (this.queue.isEmpty()) {
                        try {
                            this.wait();
                        } catch (final InterruptedException ex) {
                            // Ignore
                        }
                    }

                    queueClone = new String[this.queue.size()];
                    this.queue.copyInto(queueClone);
                    this.queue.removeAllElements();

                    streamsClone = new OutputStream[outputStreams.size()];
                    outputStreams.copyInto(streamsClone);
                }

                String s = null;
                OutputStream os = null;
                if (this.start >= streamsClone.length) {
                    this.start = 0;
                }

                // Send everything in buffer in fair turn-around of the first output stream
                for (int i = 0; i < queueClone.length; i++) {
                    s = queueClone[i];
                    Logger.log("Server: Sending " + s + " ... ");

                    for (int m = this.start; m < streamsClone.length; m++) {
                        os = streamsClone[m];
                        sendHelper(os, s);
                        Logger.log(" ... to outputstream #" + m);
                    }

                    if (this.start != 0) {
                        for (int n = 0; start < streamsClone.length && n < this.start; n++) {
                            os = streamsClone[n];
                            sendHelper(os, s);
                            Logger.log(" ... to outputstream #" + n);
                        }
                    }
                    if ((this.start + 1) >= streamsClone.length) {
                        start = 0;
                    } else {
                        this.start++;
                    }
                }
            }
        }


        /** Helper method to send to output streams and handle events. */
        private synchronized void sendHelper(final OutputStream os, final String s) {
            try {
                os.write(s.getBytes());
                os.flush();
            } catch (final IOException ex) {
                if (outputStreams.contains(os)) {
                    outputStreams.removeElement(os);
                }
            }
        }


        /** Helper method to set data from the outside. */
        private synchronized void setSendData(final String s) {
            if (s == null) {
                throw new NullPointerException("s must not be null.");
            }

            this.queue.addElement(s);
            this.notifyAll();
        }

    }


    /**
     * Returns if the device supports bluetooth resp. if bluetooth is turned on.
     * @return
     */
    public static boolean supportsBluetooth() {
        return supportsBluetooth;
    }

}

/* End of BTServerNetworkManager.java */
